package com.seventhdawn.gwt.rcx.server;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

import com.google.gwt.user.client.rpc.IsSerializable;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.server.rpc.RPCServletUtils;
import com.google.gwt.user.server.rpc.impl.ServerSerializationStreamReader;
import com.seventhdawn.gwt.rcx.client.RPCContextServiceProxy;

/**
 * Filter that handles RPC context object deserialization prior to processing
 * the actual RPC call.
 * <p>
 * <b>Important:</b> This filter needs to be applied <em>before
 * your server-side RPC service implementation</em> (e.g. a
 * {@link com.google.gwt.user.server.rpc.RemoteServiceServlet
 * RemoteServiceServlet} subclass) takes over the RPC request processing.
 * <p>
 * RPC service methods can access the deserialized context object via
 * {@link RPCServerContext}.
 * 
 * @see RPCServerContext
 * @see RPCContextServiceProxy
 * @author vojtech.szocs
 */
public class RPCContextFilter extends RPCSerializationPolicyProvider implements Filter {

    private static final Logger logger = Logger.getLogger(RPCContextFilter.class.getName());

    /**
     * Name of initialization parameter that specifies whether a missing RPC
     * context payload (indicated by a special
     * {@link RPCContextServiceProxy#HEADER_PAYLOAD_LENGTH header}) should be
     * ignored.
     * <p>
     * Possible values: {@code true}, {@code false}.
     * <p>
     * This parameter is optional (default value is {@code false}).
     */
    public static final String IGNORE_MISSING_PAYLOAD = "ignoreMissingPayload";

    /**
     * Name of initialization parameter that specifies whether serialization
     * errors during RPC context object deserialization should be ignored.
     * <p>
     * Possible values: {@code true}, {@code false}.
     * <p>
     * This parameter is optional (default value is {@code false}).
     */
    public static final String IGNORE_SERIALIZATION_ERRORS = "ignoreSerializationErrors";

    private FilterConfig filterConfig;
    private String requestContextPath;

    public void init(FilterConfig filterConfig) throws ServletException {
        this.filterConfig = filterConfig;
    }

    public void destroy() {
        filterConfig = null;
    }

    @Override
    protected ServletContext getServletContext() {
        return filterConfig.getServletContext();
    }

    @Override
    protected String getRequestContextPath() {
        return requestContextPath;
    }

    private void setRequestContextPath(String requestContextPath) {
        this.requestContextPath = requestContextPath;
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (filterConfig == null)
            throw new ServletException("doFilter() called before init()");

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        setRequestContextPath(httpRequest.getContextPath());

        try {
            // filter incoming request and pass it along the filter chain
            HttpServletRequest filteredRequest = processPayload(httpRequest);
            chain.doFilter(filteredRequest, response);
        } finally {
            // nullify thread-local variables
            RPCServerContext.set(null);
        }
    }

    protected boolean getBooleanInitParam(String paramName, boolean defaultValue) {
        String paramValue = filterConfig.getInitParameter(paramName);

        if (paramValue != null)
            return Boolean.valueOf(paramValue);
        else
            return defaultValue;
    }

    /**
     * Process RPC context payload that comes along the given HTTP request.
     * <p>
     * Return filtered request to be passed to the next entity in the filter
     * chain.
     */
    protected HttpServletRequest processPayload(HttpServletRequest request) throws IOException, ServletException {
        boolean ignoreMissingPayload = getBooleanInitParam(IGNORE_MISSING_PAYLOAD, false);
        boolean ignoreSerializationErrors = getBooleanInitParam(IGNORE_SERIALIZATION_ERRORS, false);

        // extract RPC context payload
        String contextPayloadLength = request.getHeader(RPCContextServiceProxy.HEADER_PAYLOAD_LENGTH);
        if (contextPayloadLength == null) {
            if (ignoreMissingPayload) {
                logger.log(Level.WARNING, "Ignoring missing RPC context payload");
                return request;
            } else {
                throw new ServletException("Missing RPC context payload");
            }
        }

        String requestBody = RPCServletUtils.readContentAsUtf8(request, true);
        int contextPayloadOffset = requestBody.length() - Integer.valueOf(contextPayloadLength);
        String contextPayload = requestBody.substring(contextPayloadOffset);

        // deserialize RPC context object
        deserializeContextObject(contextPayload, ignoreSerializationErrors);

        // split default RPC payload apart from RPC context payload
        final byte[] defaultRpcPayload = requestBody.substring(0, contextPayloadOffset).getBytes();
        final ByteArrayInputStream defaultRpcInputStream = new ByteArrayInputStream(defaultRpcPayload);

        return new HttpServletRequestWrapper(request) {

            @Override
            public int getContentLength() {
                return defaultRpcPayload.length;
            }

            @Override
            public ServletInputStream getInputStream() throws IOException {
                return new ServletInputStream() {

                    @Override
                    public int available() throws IOException {
                        return defaultRpcInputStream.available();
                    }

                    @Override
                    public int read() throws IOException {
                        return defaultRpcInputStream.read();
                    }

                    @Override
                    public int read(byte[] b, int off, int len) throws IOException {
                        return defaultRpcInputStream.read(b, off, len);
                    }

                };
            }

        };
    }

    protected void deserializeContextObject(String contextPayload, boolean ignoreSerializationErrors) throws ServletException {
        try {
            ServerSerializationStreamReader streamReader = new ServerSerializationStreamReader(Thread.currentThread().getContextClassLoader(), this);
            streamReader.prepareToRead(contextPayload);
            IsSerializable contextObject = (IsSerializable) streamReader.readObject();
            RPCServerContext.set(contextObject);
        } catch (SerializationException sex) {
            if (ignoreSerializationErrors) {
                logger.log(Level.WARNING, "Ignoring RPC context object deserialization error", sex);
            } else {
                throw new ServletException("Failed to deserialize RPC context object", sex);
            }
        }
    }

}
